Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 026b8c5bc7855095c2eb4e1cc43329493f8bad57


Parents : 9c739fa
Author : Ivan <e46112d44649266d71fe2193e00a4710>
Signature : T66BB85Valid, signed by author
Date : 2026-07-23T08:47:54-05:00

feat: update LXST version to 0.5.0 for android, fix telephone initialization error handling, and add compatibility layer for LXST profiles across versions

Changes
Diff

diff --git a/android/app/build.gradle b/android/app/build.gradle
index 37678117..f4b0f306 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -11,7 +11,7 @@ def repositoryBundledWheelsDir = new File(meshchatSourceDir, "public/repository-
def chaquopyBuildPython = System.getenv("CHAQUOPY_BUILD_PYTHON")
def pythonTempDir = new File(buildDir, "python-tmp")
def vendorWheelDir = new File(rootProject.projectDir, "vendor")
-def lxstPatchedWheel = new File(rootProject.projectDir, "vendor/lxst-0.4.8-py3-none-any.whl")
+def lxstPatchedWheel = new File(rootProject.projectDir, "vendor/lxst-0.5.0-py3-none-any.whl")
def bleakPatchedWheel = new File(rootProject.projectDir, "vendor/bleak-3.0.2-py3-none-any.whl")
def rnsPatchedWheel = new File(rootProject.projectDir, "vendor/rns-1.4.0-py3-none-any.whl")
def allAndroidAbis = ["arm64-v8a", "x86_64", "armeabi-v7a"]

diff --git a/meshchatx.rsm b/meshchatx.rsm
index 6c63c443..012be9e8 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ

diff --git a/meshchatx/src/backend/http/routes/telephone.py b/meshchatx/src/backend/http/routes/telephone.py
index a369ae40..68b393a9 100644
--- a/meshchatx/src/backend/http/routes/telephone.py
+++ b/meshchatx/src/backend/http/routes/telephone.py
@@ -249,12 +249,12 @@ def register_telephone_routes(routes, app):
"path_hops": None,
"path_interface": None,
}
- from LXST.Primitives.Telephony import Profiles
+ from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
mode_id = active_call["call_mode_id"]
with contextlib.suppress(Exception):
- active_call["call_mode_name"] = Profiles.mode_name(mode_id)
- active_call["call_mode_abbrev"] = Profiles.mode_abbrevation(mode_id)
+ active_call["call_mode_name"] = lxst_modes.mode_name(mode_id)
+ active_call["call_mode_abbrev"] = lxst_modes.mode_abbreviation(mode_id)
link = getattr(app.telephone_manager, "call_stats", {}).get("link")
if link:
active_call["tx_bytes"] = getattr(link, "txbytes", 0)
@@ -497,20 +497,21 @@ def register_telephone_routes(routes, app):
@routes.get("/api/v1/telephone/call-modes")
async def telephone_call_modes(request):
- from LXST.Primitives.Telephony import Profiles
+ from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
+ half = lxst_modes.mode_half_duplex()
modes = [
{
"id": mode_id,
- "name": Profiles.mode_name(mode_id),
- "abbrev": Profiles.mode_abbrevation(mode_id),
- "is_half_duplex": mode_id == Profiles.MODE_HALF_DUPLEX,
+ "name": lxst_modes.mode_name(mode_id),
+ "abbrev": lxst_modes.mode_abbreviation(mode_id),
+ "is_half_duplex": mode_id == half,
}
- for mode_id in Profiles.available_modes()
+ for mode_id in lxst_modes.available_modes()
]
return web.json_response(
{
- "default_call_mode_id": Profiles.DEFAULT_MODE,
+ "default_call_mode_id": lxst_modes.default_mode(),
"call_modes": modes,
},
)
@@ -529,14 +530,14 @@ def register_telephone_routes(routes, app):
int(mode_id),
)
app.config.telephone_call_mode_id.set(resolved)
- from LXST.Primitives.Telephony import Profiles
+ from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
return web.json_response(
{
"message": f"Switched to mode {resolved}",
"mode_id": resolved,
- "mode_name": Profiles.mode_name(resolved),
- "is_half_duplex": resolved == Profiles.MODE_HALF_DUPLEX,
+ "mode_name": lxst_modes.mode_name(resolved),
+ "is_half_duplex": resolved == lxst_modes.mode_half_duplex(),
"is_ptt_active": bool(app.telephone_manager.ptt_active),
},
)

diff --git a/meshchatx/src/backend/identity_context.py b/meshchatx/src/backend/identity_context.py
index c07e6857..4d3aac7d 100644
--- a/meshchatx/src/backend/identity_context.py
+++ b/meshchatx/src/backend/identity_context.py
@@ -363,9 +363,15 @@ class IdentityContext:
)
if not getattr(self.app, "emergency", False):
- self.telephone_manager.init_telephone()
- with contextlib.suppress(Exception):
- self.app.sync_telephone_call_policy(context=self)
+ # Telephony must not take down mesh startup. Android may ship an
+ # older LXST without duplex helpers, and audio backends can fail
+ # while LXMF/RNS remain usable.
+ try:
+ self.telephone_manager.init_telephone()
+ with contextlib.suppress(Exception):
+ self.app.sync_telephone_call_policy(context=self)
+ except Exception as exc:
+ print(f"Telephone init failed (mesh continues): {exc}", flush=True)
self.voicemail_manager = VoicemailManager(
db=self.database,

diff --git a/meshchatx/src/backend/lxst_profiles_compat.py b/meshchatx/src/backend/lxst_profiles_compat.py
new file mode 100644
index 00000000..e36545ec
--- /dev/null
+++ b/meshchatx/src/backend/lxst_profiles_compat.py
@@ -0,0 +1,79 @@
+# SPDX-License-Identifier: 0BSD
+
+"""LXST Profiles helpers that work across LXST versions.
+
+Android previously vendored lxst 0.4.8 without call-mode helpers
+(available_modes, MODE_FULL_DUPLEX, MODE_HALF_DUPLEX). Desktop and current
+Android builds ship 0.5.0+. MeshChatX keeps this shim so identity setup never
+crashes when an older Profiles class is present.
+"""
+
+from __future__ import annotations
+
+# Match LXST 0.5+ Profiles constants when the installed package lacks them.
+MODE_FULL_DUPLEX = 0x01
+MODE_HALF_DUPLEX = 0x02
+DEFAULT_MODE = MODE_FULL_DUPLEX
+
+_MODE_NAMES = {
+ MODE_FULL_DUPLEX: "Full Duplex",
+ MODE_HALF_DUPLEX: "Half Duplex",
+}
+_MODE_ABBREVS = {
+ MODE_FULL_DUPLEX: "FDX",
+ MODE_HALF_DUPLEX: "HDX",
+}
+
+
+def import_profiles():
+ """Import LXST Profiles, or raise ImportError if LXST is unavailable."""
+ from LXST.Primitives.Telephony import Profiles
+
+ return Profiles
+
+
+def duplex_modes_supported(profiles=None) -> bool:
+ """True when the installed LXST Profiles exposes call-mode helpers."""
+ Profiles = profiles if profiles is not None else import_profiles()
+ return callable(getattr(Profiles, "available_modes", None))
+
+
+def mode_full_duplex(profiles=None) -> int:
+ Profiles = profiles if profiles is not None else import_profiles()
+ return int(getattr(Profiles, "MODE_FULL_DUPLEX", MODE_FULL_DUPLEX))
+
+
+def mode_half_duplex(profiles=None) -> int:
+ Profiles = profiles if profiles is not None else import_profiles()
+ return int(getattr(Profiles, "MODE_HALF_DUPLEX", MODE_HALF_DUPLEX))
+
+
+def default_mode(profiles=None) -> int:
+ Profiles = profiles if profiles is not None else import_profiles()
+ return int(getattr(Profiles, "DEFAULT_MODE", DEFAULT_MODE))
+
+
+def available_modes(profiles=None) -> list[int]:
+ """Return duplex mode ids supported by the installed LXST build."""
+ Profiles = profiles if profiles is not None else import_profiles()
+ native = getattr(Profiles, "available_modes", None)
+ if callable(native):
+ return list(native())
+ # Pre-duplex LXST: full duplex only. Half duplex needs switch_mode / squelch.
+ return [mode_full_duplex(Profiles)]
+
+
+def mode_name(mode_id, profiles=None) -> str:
+ Profiles = profiles if profiles is not None else import_profiles()
+ native = getattr(Profiles, "mode_name", None)
+ if callable(native):
+ return str(native(mode_id))
+ return _MODE_NAMES.get(int(mode_id), "Default")
+
+
+def mode_abbreviation(mode_id, profiles=None) -> str:
+ Profiles = profiles if profiles is not None else import_profiles()
+ native = getattr(Profiles, "mode_abbrevation", None)
+ if callable(native):
+ return str(native(mode_id))
+ return _MODE_ABBREVS.get(int(mode_id), "DFLT")

diff --git a/meshchatx/src/backend/telephone_manager.py b/meshchatx/src/backend/telephone_manager.py
index d7365724..c2075828 100644
--- a/meshchatx/src/backend/telephone_manager.py
+++ b/meshchatx/src/backend/telephone_manager.py
@@ -171,9 +171,9 @@ class TelephoneManager:
def resolve_call_mode_id(self, mode_id=None):
"""Return a valid LXST call mode id (full or half duplex)."""
- from LXST.Primitives.Telephony import Profiles
+ from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
- available = set(Profiles.available_modes())
+ available = set(lxst_modes.available_modes())
mid = mode_id
if mid is None and self.config_manager:
with contextlib.suppress(Exception):
@@ -183,7 +183,7 @@ class TelephoneManager:
except (TypeError, ValueError):
mid = None
if mid not in available:
- mid = Profiles.DEFAULT_MODE
+ mid = lxst_modes.default_mode()
return mid
def apply_preferred_mode(self, mode_id=None):
@@ -199,7 +199,7 @@ class TelephoneManager:
def switch_mode(self, mode_id):
"""Switch live call duplex mode via LXST signalling and local squelch."""
- from LXST.Primitives.Telephony import Profiles
+ from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
resolved = self.resolve_call_mode_id(mode_id)
self.preferred_mode_id = resolved
@@ -216,7 +216,7 @@ class TelephoneManager:
self.telephone.active_call.call_mode = resolved
# Half duplex starts listen-only. Full duplex keeps continuous TX open.
self.ptt_active = False
- if resolved == Profiles.MODE_HALF_DUPLEX:
+ if resolved == lxst_modes.mode_half_duplex():
with contextlib.suppress(Exception):
self.telephone.squelch_transmit(True)
else:
@@ -226,23 +226,24 @@ class TelephoneManager:
def get_active_mode_id(self):
"""Return the active call mode, preferred mode, or LXST default."""
- from LXST.Primitives.Telephony import Profiles
+ from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
+ available = set(lxst_modes.available_modes())
if self.telephone and self.telephone.active_call:
link_mode = getattr(self.telephone.active_call, "call_mode", None)
- if link_mode in Profiles.available_modes():
+ if link_mode in available:
return link_mode
mode = getattr(self.telephone, "active_mode", None)
- if mode in Profiles.available_modes():
+ if mode in available:
return mode
- if self.preferred_mode_id in Profiles.available_modes():
+ if self.preferred_mode_id in available:
return self.preferred_mode_id
return self.resolve_call_mode_id()
def is_half_duplex(self):
- from LXST.Primitives.Telephony import Profiles
+ from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
- return self.get_active_mode_id() == Profiles.MODE_HALF_DUPLEX
+ return self.get_active_mode_id() == lxst_modes.mode_half_duplex()
def is_transmit_squelched(self):
"""True when LXST packetizer is squelched (half-duplex idle / PTT up)."""

diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue
index 6465c673..05a8895a 100644
--- a/meshchatx/src/frontend/components/App.vue
+++ b/meshchatx/src/frontend/components/App.vue
@@ -23,10 +23,12 @@
:network-degraded-label="networkDegradedBannerLabel"
:network-recovering="networkRecovering"
:recover-network-label="$t('app.recover_network')"
+ :open-settings-label="$t('app.open_settings')"
:open-interfaces-label="$t('app.open_interfaces')"
@restart-backend="onRestartBackend"
@view-backend-logs="onViewBackendCrashReport"
@recover-network="onRecoverNetwork"
+ @open-settings="onOpenSettingsForRecovery"
@open-interfaces="onOpenInterfacesForRecovery"
/>
@@ -1205,6 +1207,9 @@ export default {
onOpenInterfacesForRecovery() {
this.$router.push({ name: "interfaces" });
},
+ onOpenSettingsForRecovery() {
+ this.$router.push({ name: "settings" });
+ },
async onRecoverNetwork() {
if (this.networkRecovering) {
return;

diff --git a/meshchatx/src/frontend/components/layout/AppShellBanners.vue b/meshchatx/src/frontend/components/layout/AppShellBanners.vue
index f4de78ba..f7390c83 100644
--- a/meshchatx/src/frontend/components/layout/AppShellBanners.vue
+++ b/meshchatx/src/frontend/components/layout/AppShellBanners.vue
@@ -69,6 +69,13 @@
>
{{ recoverNetworkLabel }}
</button>
+ <button
+ type="button"
+ class="rounded-md bg-white/10 px-3 py-1 text-xs font-semibold hover:bg-white/20"
+ @click="$emit('open-settings')"
+ >
+ {{ openSettingsLabel }}
+ </button>
<button
type="button"
class="rounded-md bg-white/10 px-3 py-1 text-xs font-semibold hover:bg-white/20"
@@ -152,11 +159,15 @@ export default {
type: String,
default: "",
},
+ openSettingsLabel: {
+ type: String,
+ default: "",
+ },
openInterfacesLabel: {
type: String,
default: "",
},
},
- emits: ["restart-backend", "view-backend-logs", "recover-network", "open-interfaces"],
+ emits: ["restart-backend", "view-backend-logs", "recover-network", "open-settings", "open-interfaces"],
};
</script>

diff --git a/meshchatx/src/frontend/js/networkRecovery.js b/meshchatx/src/frontend/js/networkRecovery.js
new file mode 100644
index 00000000..af24a167
--- /dev/null
+++ b/meshchatx/src/frontend/js/networkRecovery.js
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Classify mesh startup / degraded errors for recovery UI.
+ * Not every failure is an interface config problem.
+ */
+
+const INTERFACE_HINTS = [
+ "interface",
+ "interfaces",
+ "rnode",
+ "serial",
+ "usb",
+ "autointerface",
+ "tcpclient",
+ "udp",
+ "i2p",
+ "wlan",
+ "kiss",
+ "ax25",
+ "pipeinterface",
+ "rns config",
+ "configobj",
+];
+
+/**
+ * @param {unknown} error
+ * @returns {boolean}
+ */
+export function isLikelyInterfaceRecoveryError(error) {
+ const text = String(error || "").toLowerCase();
+ if (!text) {
+ return false;
+ }
+ return INTERFACE_HINTS.some((hint) => text.includes(hint));
+}
+
+/**
+ * Route name to open after degraded startup, or null to leave the user on the
+ * current page (banner actions still apply).
+ * @param {unknown} error
+ * @returns {"interfaces" | null}
+ */
+export function recoveryRouteForNetworkError(error) {
+ if (isLikelyInterfaceRecoveryError(error)) {
+ return "interfaces";
+ }
+ return null;
+}

diff --git a/meshchatx/src/frontend/js/networkStartupWait.js b/meshchatx/src/frontend/js/networkStartupWait.js
index b73c4add..20eb1e69 100644
--- a/meshchatx/src/frontend/js/networkStartupWait.js
+++ b/meshchatx/src/frontend/js/networkStartupWait.js
@@ -2,8 +2,8 @@
export const STARTUP_STAGE_LABELS = {
http: "Getting things ready…",
- starting: "Getting the mesh ready…",
- rns: "Connecting to the mesh…",
+ starting: "Getting RNS ready…",
+ rns: "Starting RNS…",
identity: "Almost there…",
ready: "Ready",
failed: "Startup failed",
@@ -29,7 +29,7 @@ export function interpretStartupStatus(data) {
if (status === "failed") {
// HTTP is up and the backend marked UI as usable: mount the app in
- // degraded mode so interfaces/settings remain reachable for recovery.
+ // degraded mode so settings/interfaces remain reachable for recovery.
if (uiReady || networkDegraded) {
return {
kind: "degraded",
@@ -48,7 +48,7 @@ export function interpretStartupStatus(data) {
}
if (status === "starting" || status === undefined) {
const resolvedStage = stage || "starting";
- const label = STARTUP_STAGE_LABELS[resolvedStage] || "Starting network…";
+ const label = STARTUP_STAGE_LABELS[resolvedStage] || "Starting RNS…";
// HTTP is bound and the shell may mount while RNS/identity finish.
if (uiReady) {
return {
@@ -101,7 +101,7 @@ export async function waitForNetworkReady(options = {}) {
const data = await response.json();
const interpreted = interpretStartupStatus(data);
if (interpreted.kind === "degraded") {
- onLine(interpreted.error || "Mesh network unavailable. Opening recovery UI…");
+ onLine(interpreted.error || "RNS unavailable. Opening recovery UI…");
onDegraded(interpreted.error);
return "degraded";
}

diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index 564cfaeb..488686ac 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -491,12 +491,13 @@
"rpc_key_hide": "RPC-Schlüssel verbergen",
"refresh_community_interfaces": "Von directory.rns.recipes aktualisieren",
"refresh_community_interfaces_busy": "Wird aktualisiert…",
- "network_starting": "Verbinde mit dem Mesh…",
- "network_degraded": "Mesh-Netzwerk nicht verfügbar. Die App läuft weiter, damit Sie Schnittstellen reparieren können, ohne Daten zu löschen.",
- "recover_network": "Netzwerk erneut versuchen",
+ "network_starting": "RNS wird gestartet…",
+ "network_degraded": "RNS nicht verfügbar. Die App läuft weiter, damit Sie diagnostizieren und wiederherstellen können, ohne Daten zu löschen.",
+ "recover_network": "RNS erneut versuchen",
+ "open_settings": "Einstellungen öffnen",
"open_interfaces": "Schnittstellen öffnen",
- "network_recovered": "Netzwerkstapel wiederhergestellt",
- "network_recover_failed": "Netzwerkstapel konnte nicht wiederhergestellt werden. Prüfen Sie die Schnittstellen und versuchen Sie es erneut.",
+ "network_recovered": "RNS wiederhergestellt",
+ "network_recover_failed": "RNS konnte nicht wiederhergestellt werden. Prüfen Sie die Fehlerdetails, dann erneut versuchen oder Einstellungen und Schnittstellen prüfen.",
"battery_unavailable": "Akkustatus nicht verfügbar",
"battery_level_title": "Akku {percent}",
"battery_charging_title": "Lädt {percent}",

diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index 9372bbf7..5f6b341a 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -430,12 +430,13 @@
"backend_process_stopped": "Reticulum backend stopped",
"backend_reconnected": "Reconnected to backend",
"restart_backend": "Restart backend",
- "network_starting": "Connecting to the mesh…",
- "network_degraded": "Mesh network unavailable. The app is still running so you can fix interfaces without wiping data.",
- "recover_network": "Retry network",
+ "network_starting": "Starting RNS…",
+ "network_degraded": "RNS unavailable. The app is still running so you can diagnose and recover without wiping data.",
+ "recover_network": "Retry RNS",
+ "open_settings": "Open settings",
"open_interfaces": "Open interfaces",
- "network_recovered": "Network stack recovered",
- "network_recover_failed": "Could not recover the network stack. Check interfaces and try again.",
+ "network_recovered": "RNS recovered",
+ "network_recover_failed": "Could not recover RNS. Check the error details, then retry or review settings and interfaces.",
"restart_backend_started": "Restarting backend…",
"restart_backend_failed": "Could not restart backend",
"memory_pressure_warning": "High memory use detected. Close large Nomad pages, voice notes, or downloads if you can, then dismiss this warning. Persistent pressure can come from hostile peer content.",

diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index 7955d388..c2bc6ab6 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -491,12 +491,13 @@
"rpc_key_hide": "Ocultar clave RPC",
"refresh_community_interfaces": "Actualizar desde directory.rns.recipes",
"refresh_community_interfaces_busy": "Actualizando…",
- "network_starting": "Conectando a la mesh…",
- "network_degraded": "Red mesh no disponible. La aplicación sigue en ejecución para que pueda reparar interfaces sin borrar datos.",
- "recover_network": "Reintentar red",
+ "network_starting": "Iniciando RNS…",
+ "network_degraded": "RNS no disponible. La aplicación sigue en ejecución para que pueda diagnosticar y recuperar sin borrar datos.",
+ "recover_network": "Reintentar RNS",
+ "open_settings": "Abrir configuración",
"open_interfaces": "Abrir interfaces",
- "network_recovered": "Pila de red recuperada",
- "network_recover_failed": "No se pudo recuperar la pila de red. Revise las interfaces e inténtelo de nuevo.",
+ "network_recovered": "RNS recuperado",
+ "network_recover_failed": "No se pudo recuperar RNS. Revise el error y reintente, o revise la configuración y las interfaces.",
"battery_unavailable": "Estado de batería no disponible",
"battery_level_title": "Batería {percent}",
"battery_charging_title": "Cargando {percent}",

diff --git a/meshchatx/src/frontend/locales/fi.json b/meshchatx/src/frontend/locales/fi.json
index c79c78bb..188a19a9 100644
--- a/meshchatx/src/frontend/locales/fi.json
+++ b/meshchatx/src/frontend/locales/fi.json
@@ -491,12 +491,13 @@
"rpc_key_hide": "Piilota RPC-avain",
"refresh_community_interfaces": "Päivitä lähteestä directory.rns.recipes",
"refresh_community_interfaces_busy": "Päivitetään…",
- "network_starting": "Yhdistetään mesh-verkkoon…",
- "network_degraded": "Mesh-verkko ei ole käytettävissä. Sovellus pysyy käynnissä, jotta voit korjata liittymiä ilman tietojen tyhjennystä.",
- "recover_network": "Yritä verkkoa uudelleen",
+ "network_starting": "Käynnistetään RNS…",
+ "network_degraded": "RNS ei ole käytettävissä. Sovellus pysyy käynnissä, jotta voit diagnosoida ja palauttaa ilman tietojen tyhjennystä.",
+ "recover_network": "Yritä RNS uudelleen",
+ "open_settings": "Avaa asetukset",
"open_interfaces": "Avaa liittymät",
- "network_recovered": "Verkkopinon palautus onnistui",
- "network_recover_failed": "Verkkopinoa ei voitu palauttaa. Tarkista liittymät ja yritä uudelleen.",
+ "network_recovered": "RNS palautettu",
+ "network_recover_failed": "RNS:ää ei voitu palauttaa. Tarkista virhetiedot, yritä uudelleen tai tarkista asetukset ja liittymät.",
"battery_unavailable": "Akun tila ei ole saatavilla",
"battery_level_title": "Akku {percent}",
"battery_charging_title": "Lataa {percent}",

diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index 431f24fe..936f3cf5 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -491,12 +491,13 @@
"rpc_key_hide": "Masquer la clé RPC",
"refresh_community_interfaces": "Actualiser depuis directory.rns.recipes",
"refresh_community_interfaces_busy": "Actualisation…",
- "network_starting": "Connexion au mesh…",
- "network_degraded": "Réseau mesh indisponible. L'application reste ouverte pour réparer les interfaces sans effacer les données.",
- "recover_network": "Réessayer le réseau",
+ "network_starting": "Démarrage de RNS…",
+ "network_degraded": "RNS indisponible. L'application reste ouverte pour diagnostiquer et récupérer sans effacer les données.",
+ "recover_network": "Réessayer RNS",
+ "open_settings": "Ouvrir les paramètres",
"open_interfaces": "Ouvrir les interfaces",
- "network_recovered": "Pile réseau rétablie",
- "network_recover_failed": "Impossible de rétablir la pile réseau. Vérifiez les interfaces et réessayez.",
+ "network_recovered": "RNS rétabli",
+ "network_recover_failed": "Impossible de rétablir RNS. Vérifiez l'erreur, puis réessayez ou consultez les paramètres et les interfaces.",
"battery_unavailable": "État de la batterie indisponible",
"battery_level_title": "Batterie {percent}",
"battery_charging_title": "En charge {percent}",

diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index 32017474..f4bfad99 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -491,12 +491,13 @@
"rpc_key_hide": "Nascondi chiave RPC",
"refresh_community_interfaces": "Aggiorna da directory.rns.recipes",
"refresh_community_interfaces_busy": "Aggiornamento…",
- "network_starting": "Connessione alla mesh…",
- "network_degraded": "Rete mesh non disponibile. L'app resta attiva così puoi riparare le interfacce senza cancellare i dati.",
- "recover_network": "Riprova rete",
+ "network_starting": "Avvio di RNS…",
+ "network_degraded": "RNS non disponibile. L'app resta attiva così puoi diagnosticare e recuperare senza cancellare i dati.",
+ "recover_network": "Riprova RNS",
+ "open_settings": "Apri impostazioni",
"open_interfaces": "Apri interfacce",
- "network_recovered": "Stack di rete ripristinato",
- "network_recover_failed": "Impossibile ripristinare lo stack di rete. Controlla le interfacce e riprova.",
+ "network_recovered": "RNS ripristinato",
+ "network_recover_failed": "Impossibile ripristinare RNS. Controlla l'errore, poi riprova o rivedi impostazioni e interfacce.",
"battery_unavailable": "Stato batteria non disponibile",
"battery_level_title": "Batteria {percent}",
"battery_charging_title": "In carica {percent}",

diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index 59dc80c2..940fc1fe 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -491,12 +491,13 @@
"rpc_key_hide": "RPC-sleutel verbergen",
"refresh_community_interfaces": "Vernieuwen vanaf directory.rns.recipes",
"refresh_community_interfaces_busy": "Bezig met vernieuwen…",
- "network_starting": "Verbinden met het mesh…",
- "network_degraded": "Mesh-netwerk niet beschikbaar. De app blijft draaien zodat je interfaces kunt herstellen zonder gegevens te wissen.",
- "recover_network": "Netwerk opnieuw proberen",
+ "network_starting": "RNS starten…",
+ "network_degraded": "RNS niet beschikbaar. De app blijft draaien zodat je kunt diagnosticeren en herstellen zonder gegevens te wissen.",
+ "recover_network": "RNS opnieuw proberen",
+ "open_settings": "Instellingen openen",
"open_interfaces": "Interfaces openen",
- "network_recovered": "Netwerkstack hersteld",
- "network_recover_failed": "Netwerkstack kon niet worden hersteld. Controleer interfaces en probeer opnieuw.",
+ "network_recovered": "RNS hersteld",
+ "network_recover_failed": "RNS kon niet worden hersteld. Controleer de fout, probeer opnieuw of bekijk instellingen en interfaces.",
"battery_unavailable": "Batterijstatus niet beschikbaar",
"battery_level_title": "Batterij {percent}",
"battery_charging_title": "Opladen {percent}",

diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index 28163689..6839c74b 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -491,12 +491,13 @@
"rpc_key_hide": "Скрыть RPC-ключ",
"refresh_community_interfaces": "Обновить из directory.rns.recipes",
"refresh_community_interfaces_busy": "Обновление…",
- "network_starting": "Подключение к mesh…",
- "network_degraded": "Mesh-сеть недоступна. Приложение продолжает работать, чтобы вы могли исправить интерфейсы без удаления данных.",
- "recover_network": "Повторить сеть",
+ "network_starting": "Запуск RNS…",
+ "network_degraded": "RNS недоступен. Приложение продолжает работать, чтобы вы могли диагностировать и восстановить работу без удаления данных.",
+ "recover_network": "Повторить RNS",
+ "open_settings": "Открыть настройки",
"open_interfaces": "Открыть интерфейсы",
- "network_recovered": "Сетевой стек восстановлен",
- "network_recover_failed": "Не удалось восстановить сетевой стек. Проверьте интерфейсы и попробуйте снова.",
+ "network_recovered": "RNS восстановлен",
+ "network_recover_failed": "Не удалось восстановить RNS. Проверьте сведения об ошибке, затем повторите попытку или проверьте настройки и интерфейсы.",
"battery_unavailable": "Состояние батареи недоступно",
"battery_level_title": "Батарея {percent}",
"battery_charging_title": "Зарядка {percent}",

diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index c8fc9d66..e6749dea 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -491,12 +491,13 @@
"rpc_key_hide": "隐藏 RPC 密钥",
"refresh_community_interfaces": "从 directory.rns.recipes 刷新",
"refresh_community_interfaces_busy": "正在刷新…",
- "network_starting": "正在连接 mesh…",
- "network_degraded": "Mesh 网络不可用。应用仍在运行,便于修复接口而无需清除数据。",
- "recover_network": "重试网络",
+ "network_starting": "正在启动 RNS…",
+ "network_degraded": "RNS 不可用。应用仍在运行,便于诊断和恢复而无需清除数据。",
+ "recover_network": "重试 RNS",
+ "open_settings": "打开设置",
"open_interfaces": "打开接口",
- "network_recovered": "网络栈已恢复",
- "network_recover_failed": "无法恢复网络栈。请检查接口后重试。",
+ "network_recovered": "RNS 已恢复",
+ "network_recover_failed": "无法恢复 RNS。请查看错误详情后重试,或检查设置与接口。",
"battery_unavailable": "无法获取电池状态",
"battery_level_title": "电池 {percent}",
"battery_charging_title": "充电中 {percent}",

diff --git a/meshchatx/src/frontend/main.js b/meshchatx/src/frontend/main.js
index e202371d..989ecade 100644
--- a/meshchatx/src/frontend/main.js
+++ b/meshchatx/src/frontend/main.js
@@ -18,6 +18,7 @@ import { registerCoreContributions } from "./js/registries/registerCoreContribut
import { installWsEventBridge } from "./js/registries/wsEventBridge.js";
import { pluginHost } from "./js/plugins/PluginHost.js";
import GlobalState from "./js/GlobalState.js";
+import { recoveryRouteForNetworkError } from "./js/networkRecovery.js";
import "./js/HeapMonitor.js";
registerCoreContributions();
@@ -360,7 +361,7 @@ const networkReady = await waitForNetworkReady({
onErrorState: markBootSplashError,
onDegraded: (error) => {
GlobalState.networkDegraded = true;
- GlobalState.networkDegradedError = error || "Mesh network unavailable";
+ GlobalState.networkDegradedError = error || "RNS unavailable";
GlobalState.networkStarting = false;
GlobalState.networkReady = false;
},
@@ -491,7 +492,7 @@ if (networkReady) {
onLine: () => {},
onDegraded: (error) => {
GlobalState.networkDegraded = true;
- GlobalState.networkDegradedError = error || "Mesh network unavailable";
+ GlobalState.networkDegradedError = error || "RNS unavailable";
GlobalState.networkStarting = false;
GlobalState.networkReady = false;
},
@@ -510,16 +511,18 @@ if (networkReady) {
GlobalState.networkStarting = false;
GlobalState.networkReady = false;
GlobalState.networkDegraded = true;
- GlobalState.networkDegradedError =
- GlobalState.networkDegradedError || "Mesh network startup timed out";
+ GlobalState.networkDegradedError = GlobalState.networkDegradedError || "RNS startup timed out";
}
});
}
if (GlobalState.networkDegraded) {
- try {
- router.replace({ name: "interfaces" });
- } catch {
- // Route may not exist yet during early boot, but the banner still guides the user.
+ const recoveryRoute = recoveryRouteForNetworkError(GlobalState.networkDegradedError);
+ if (recoveryRoute) {
+ try {
+ router.replace({ name: recoveryRoute });
+ } catch {
+ // Route may not exist yet during early boot, but the banner still guides the user.
+ }
}
}
}

diff --git a/scripts/build-android-wheels-local.sh b/scripts/build-android-wheels-local.sh
index 9aebc124..779d2607 100755
--- a/scripts/build-android-wheels-local.sh
+++ b/scripts/build-android-wheels-local.sh
@@ -28,7 +28,7 @@ Options:
--api-level N Android API level for wheel tag (default: 24)
--pycodec2-version V pycodec2 version to build (default: 4.1.1)
--numpy-version V NumPy version used during pycodec2 build (default: 1.26.2)
- --lxst-version V LXST wheel version for metadata patch (default: 0.4.8)
+ --lxst-version V LXST wheel version for metadata patch (default: 0.5.0)
--bleak-version V bleak pure-python wheel version to vendor (default: 3.0.2)
--rns-version V rns wheel version to patch (default: 1.4.0)
--no-lxst-patch Skip LXST metadata patch
@@ -58,7 +58,7 @@ API_LEVEL="24"
PYCODEC2_VERSION="4.1.1"
LIBCODEC2_VERSION="1.2.0"
NUMPY_VERSION="1.26.2"
-LXST_VERSION="0.4.8"
+LXST_VERSION="0.5.0"
BLEAK_VERSION="3.0.2"
RNS_VERSION="1.4.0"
PATCH_LXST="1"
@@ -784,6 +784,9 @@ with zipfile.ZipFile(src, "r") as zin, zipfile.ZipFile(dst, "w", compression=zip
text = data.decode("utf-8")
text = text.replace("Requires-Dist: numpy>=2.3.4", "Requires-Dist: numpy==${NUMPY_VERSION}")
text = text.replace("Requires-Dist: cffi==1.15.1", "Requires-Dist: cffi>=1.15.1")
+ # LXST 0.5.0 pins cffi>=2.0.0. Chaquopy Android may resolve older
+ # cffi wheels, so keep the lower bound flexible like prior releases.
+ text = text.replace("Requires-Dist: cffi>=2.0.0", "Requires-Dist: cffi>=1.15.1")
data = text.encode("utf-8")
zout.writestr(item, data)
PY

diff --git a/tests/backend/test_lxst_profiles_compat.py b/tests/backend/test_lxst_profiles_compat.py
new file mode 100644
index 00000000..bd6caf54
--- /dev/null
+++ b/tests/backend/test_lxst_profiles_compat.py
@@ -0,0 +1,53 @@
+# SPDX-License-Identifier: 0BSD
+
+"""Compat for LXST Profiles call modes on Android 0.4.x vs duplex builds."""
+
+from types import SimpleNamespace
+
+from meshchatx.src.backend import lxst_profiles_compat as lxst_modes
+
+
+def test_legacy_profiles_without_modes_falls_back_to_full_duplex():
+ legacy = SimpleNamespace(
+ BANDWIDTH_LOW=0x30,
+ # No MODE_* or available_modes on Android lxst 0.4.8
+ )
+ assert lxst_modes.duplex_modes_supported(legacy) is False
+ assert lxst_modes.available_modes(legacy) == [lxst_modes.MODE_FULL_DUPLEX]
+ assert lxst_modes.default_mode(legacy) == lxst_modes.MODE_FULL_DUPLEX
+ assert lxst_modes.mode_name(lxst_modes.MODE_FULL_DUPLEX, legacy) == "Full Duplex"
+ assert lxst_modes.mode_abbreviation(lxst_modes.MODE_HALF_DUPLEX, legacy) == "HDX"
+
+
+def test_native_profiles_modes_are_forwarded():
+ native = SimpleNamespace(
+ MODE_FULL_DUPLEX=1,
+ MODE_HALF_DUPLEX=2,
+ DEFAULT_MODE=1,
+ available_modes=lambda: [1, 2],
+ mode_name=lambda mid: f"mode-{mid}",
+ mode_abbrevation=lambda mid: f"m{mid}",
+ )
+ assert lxst_modes.duplex_modes_supported(native) is True
+ assert lxst_modes.available_modes(native) == [1, 2]
+ assert lxst_modes.mode_name(2, native) == "mode-2"
+ assert lxst_modes.mode_abbreviation(1, native) == "m1"
+
+
+def test_telephone_manager_resolves_modes_on_legacy_profiles(monkeypatch):
+ from unittest.mock import MagicMock
+
+ from meshchatx.src.backend.telephone_manager import TelephoneManager
+
+ legacy = SimpleNamespace()
+ monkeypatch.setattr(
+ "meshchatx.src.backend.lxst_profiles_compat.import_profiles",
+ lambda: legacy,
+ )
+ tm = TelephoneManager(identity=MagicMock())
+ assert tm.resolve_call_mode_id(None) == lxst_modes.MODE_FULL_DUPLEX
+ assert (
+ tm.resolve_call_mode_id(lxst_modes.MODE_HALF_DUPLEX)
+ == lxst_modes.MODE_FULL_DUPLEX
+ )
+ assert tm.is_half_duplex() is False

diff --git a/tests/frontend/behaviorContracts.test.js b/tests/frontend/behaviorContracts.test.js
index 6f2f1500..5630e1da 100644
--- a/tests/frontend/behaviorContracts.test.js
+++ b/tests/frontend/behaviorContracts.test.js
@@ -299,6 +299,8 @@ describe("behavior contracts: phased startup and early UI mount", () => {
const banners = readSource("meshchatx/src/frontend/components/layout/AppShellBanners.vue");
expect(banners).toContain("showNetworkStarting");
expect(banners).toContain("networkStartingLabel");
+ expect(banners).toContain("open-settings");
+ expect(banners).toContain("open-interfaces");
});
it("backend publishes ui_ready early and defers secondary identity services", () => {

diff --git a/tests/frontend/fixtures/frontend_mega_page_ownership.json b/tests/frontend/fixtures/frontend_mega_page_ownership.json
index 3d1a6c70..d85f9db8 100644
--- a/tests/frontend/fixtures/frontend_mega_page_ownership.json
+++ b/tests/frontend/fixtures/frontend_mega_page_ownership.json
@@ -47,10 +47,7 @@
{
"id": "map",
"shell": "meshchatx/src/frontend/components/map/MapPage.vue",
- "allowed_child_dirs": [
- "meshchatx/src/frontend/components/map/internal",
- "meshchatx/src/frontend/js"
- ],
+ "allowed_child_dirs": ["meshchatx/src/frontend/components/map/internal", "meshchatx/src/frontend/js"],
"required_children": [
"meshchatx/src/frontend/components/map/internal/MapClusterPanel.vue",
"meshchatx/src/frontend/components/map/internal/MapMarkerPanel.vue",
@@ -161,12 +158,7 @@
"meshchatx/src/frontend/components/layout/AppIdentitySwitchOverlay.vue",
"meshchatx/src/frontend/components/call/CallOverlay.vue"
],
- "required_shell_imports": [
- "AppShellBanners",
- "AppIdentitySwitchOverlay",
- "CallOverlay",
- "listNavItems"
- ]
+ "required_shell_imports": ["AppShellBanners", "AppIdentitySwitchOverlay", "CallOverlay", "listNavItems"]
}
]
}

diff --git a/tests/frontend/networkRecovery.test.js b/tests/frontend/networkRecovery.test.js
new file mode 100644
index 00000000..a0dff154
--- /dev/null
+++ b/tests/frontend/networkRecovery.test.js
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: 0BSD
+
+import { describe, expect, it } from "vitest";
+
+import {
+ isLikelyInterfaceRecoveryError,
+ recoveryRouteForNetworkError,
+} from "../../meshchatx/src/frontend/js/networkRecovery.js";
+
+describe("networkRecovery", () => {
+ it("does not treat telephony or generic errors as interface recovery", () => {
+ expect(isLikelyInterfaceRecoveryError("type object 'Profiles' has no attribute 'available_modes'")).toBe(false);
+ expect(recoveryRouteForNetworkError("Telephone init failed")).toBeNull();
+ expect(recoveryRouteForNetworkError("")).toBeNull();
+ });
+
+ it("routes interface-shaped errors to the interfaces page", () => {
+ expect(isLikelyInterfaceRecoveryError("RNode interface failed to open")).toBe(true);
+ expect(recoveryRouteForNetworkError("AutoInterface bind error")).toBe("interfaces");
+ expect(recoveryRouteForNetworkError("I2P interface unavailable")).toBe("interfaces");
+ });
+});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────